package org.dmcs.homework.statistics.implementations;

import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.TreeMap;
import java.util.concurrent.ExecutorService;

import org.dmcs.homework.statistics.CollectionPrinter;
import org.dmcs.homework.statistics.api.ICombinationsGenerator;
import org.dmcs.homework.statistics.api.IFeatureSetSelector;
import org.dmcs.homework.statistics.api.IFischerCoefficientCalculator;

import Jama.Matrix;

/**
 * A tool for selecting an optimal set of dimensions to optimally separate a set of
 * given classes over a given data set.
 * @author Zielony
 * @version 1.0
 */
public class FeatureSetSelector implements IFeatureSetSelector {
	/**
	 * A Fischer Coefficient calculator.
	 */
	protected IFischerCoefficientCalculator fischerCalculator;
	
	/**
	 * A combinations generator.
	 */
	protected ICombinationsGenerator combinationsGenerator;
	
	/**
	 * A fixed thread pool to execute calculations concurrently.
	 */
	protected ExecutorService pool;
	
	/**
	 * A map relating obtained Fischer Coefficients to the feature sets connected
	 * to them and thus enabling the selection of a locally optimal feature set.
	 */
	protected Map<Double, int[]> fischerCoefficientsMap = new TreeMap<Double, int[]>();
	
	protected long startTime;
	
	protected long timeout = Long.MAX_VALUE;
	
	public FeatureSetSelector(IFischerCoefficientCalculator fischerCalculator,
			ICombinationsGenerator combinationsGenerator) {
		//TODO: Wyjatki przy niepoprawnych danych wejsciowych.
		
		this.fischerCalculator = fischerCalculator;
		this.combinationsGenerator = combinationsGenerator;
	}
	
	public FeatureSetSelector(long timeout, IFischerCoefficientCalculator 
			fischerCalculator, ICombinationsGenerator combinationsGenerator) {
		//TODO: Wyjatki przy niepoprawnych danych wejsciowych.
		
		this.fischerCalculator = fischerCalculator;
		this.combinationsGenerator = combinationsGenerator;
		this.timeout = timeout;
	}
	
	/**
	 * {@inheritDoc}
	 */
	public int[] selectOptimalFeatureSet(Matrix testData, int[][] classes) {
		//TODO: Wyjatki danych wejsciowych.
		
		startTime = System.currentTimeMillis();
		
		/*
		 * Rozpoczecie sprawdzania od opcji, w której stosujemy wszystkie dostepne
		 * wymiary.
		 */
		int[] originalDimensions = new int[testData.getRowDimension()];
		for(int i=0; i<originalDimensions.length; i++) {
			originalDimensions[i] = i;
		}
		
		/*
		 * Uruchomienie algorytmu rekursywnego startujac ze zbiorem wszystkich cech
		 * jako cechami badanymi i najnizszym mozliwym wspolczynnikiem Fischera.
		 */
		verifyFeatureSet(testData, classes, originalDimensions, 
				Double.NEGATIVE_INFINITY);
		
		/*
		 * Po ukończeniu wszystkich obliczeń: wybór najwyższego wspóczynnika Fischera.
		 */
		System.out.println("Coefficient Map:");
		System.out.println(fischerCoefficientsMap);
		
		Double maximalFischerCoefficient = Collections.max(fischerCoefficientsMap.keySet());
		
		/*
		 * Zwrócenie zbioru wymiarów, dla których wspólczynnik Fischera byl najwiekszy
		 * jako optymalnego zbioru cech. 
		 */
		return fischerCoefficientsMap.get(maximalFischerCoefficient);
	}
	
	public void verifyFeatureSet(Matrix testData, int[][] classes, int[] featureSet,
			double superFischer) {
		/*
		 * Obliczanie czasu, który minal od poczatku obliczen.
		 */
		long timeElapsed = System.currentTimeMillis() - startTime;
		
		System.out.println("Timeout: " + timeout);
		System.out.println("Time Elapsed: " + timeElapsed);
		
		/*
		 * Jesli timeout zostal przekroczony, konczenie.
		 */
		if(timeElapsed >= timeout) {
			System.out.println("ABORT");
			return;
		}
		
		//KLUDGE: REMOVE
		CollectionPrinter printer = new CollectionPrinter();
		System.out.println("Features( " + featureSet.length + "):");
		printer.printArray(featureSet);
		
		
		/*
		 * Jesli dla danej galezi drzewa algorytmu wszystkie cechy zostaly juz 
		 * przebadane - konczenie dzialania.
		 */
		if(featureSet.length == 0) {
			return;
		}
		
		//System.out.println("Feature Set: ");
		//new CollectionPrinter().printArray(featureSet);
		
		
		/*
		 * Obliczanie wspolczynnika Fischera dla danego wezla 
		 */
		double fischerCoefficient = fischerCalculator.calculateFischerCoefficient(
				testData, classes, featureSet);
		
		//KLUDGE: REMOVE
		System.out.println("Fischer Coefficient: " + fischerCoefficient);
		
		/*
		 * Jesli wspolczynnik Fischera dla obecnego wezla jest wyzszy lub rowny w
		 * porównaniu do nadrzednego, wynik jest zapisywany, a takze wezly potmne
		 *  sa rekursywnie badane.
		 */
		if(fischerCoefficient >= superFischer) {
			System.out.println("POSITIVE");
			/*
			 * Zapisanie wyniku.
			 */
			fischerCoefficientsMap.put(fischerCoefficient, featureSet);
			
			/*
			 * Kazdy z wezlow potomnych ma przypisany ten sam zbior cech minus jedna
			 * (numer cechy identyczny z numerem wezla potomnego dla porzadku).
			 */
			int[] childFeatureSet = new int[featureSet.length-1];
			
			/*
			 * Iteracja po wszystkich cechach ze zbioru cech.
			 */
			for(int i=0; i<featureSet.length; i++) {
				/*
				 * Przepisywanie wspólczynników z pominieciem i-tego.
				 */
				for(int j=0; j<featureSet.length; j++) {
					if(j<i) {
						childFeatureSet[j] = featureSet[j];
					}
					else if(j>i) {
						childFeatureSet[j-1] = featureSet[j];
					}
				}
				
				/*
				 * Rekursywne badanie wezlow potomnych z wybranymi dla nich zestawami
				 * cech oraz odniesieniem do tego wezla przez jego wspolczynnik
				 * Fischera. 
				 */
				verifyFeatureSet(testData, classes, childFeatureSet,
						fischerCoefficient);
			}
		}
		else {
			System.out.println("NEGATIVE");;
		}
	}
	
	/**
	 * {@inheritDoc}
	 */
	public int[] selectOptimalFeatureSet(Matrix testData, int[][] classes,
			int featureCount) {
		/*
		 * Wykonanie metody wymaga generatora kombinacji.
		 */
		if(this.combinationsGenerator == null) {
			throw new IllegalStateException("FeatureSetSelector|selectOptimalFeatureSet|"
					+ "combinationsGenerator cannot be null to execute this method");
		}
		
		/*
		 * Generacja wszystkich k(featureCount) - elementowych kombinacji bez
		 * powtorzen ze zbioru N(ilosc rzedow macierzy danych)-elementowego.  
		 */
		List<int[]> combinations = combinationsGenerator.getAllCombinations(
				testData.getRowDimension(), featureCount);
		
		/*
		 * Najwiekszy wspólczynnik Fischera w zbiorze - inicjalizowany minimalnym
		 * możliwym.
		 */
		double maximalFischerCoefficient = Double.NEGATIVE_INFINITY;
		
		/*
		 * Indeks kombinacji o najwiekszym wspolczynniku Fischera.
		 */
		int maximalFischerIndex = -1;
		
		/*
		 * Wspólczynnik Fischera i-tej kombinacji.
		 */
		double iFischerCoefficient;
		
		CollectionPrinter printer = new CollectionPrinter();
		
		for(int i=0; i<combinations.size(); i++) {
			/*
			 * Obliczanie wspolczynnika Fischera dla i-tej kombinacji.
			 */
			iFischerCoefficient = fischerCalculator.calculateFischerCoefficient(
					testData, classes, combinations.get(i));
		
			/*
			 * Jesli dana kombinacja ma wyzszy wspolczynnik Fischera niz wczesniejsze
			 * maksimum, podstawianie.
			 */
			if(iFischerCoefficient >= maximalFischerCoefficient) {
				maximalFischerCoefficient = iFischerCoefficient;
				
				maximalFischerIndex = i;
			}
			
			System.out.println("Considered Feature Set: ");
			
			printer.printArray(combinations.get(i));
			
			System.out.println("Fischer Coefficient: " + iFischerCoefficient);
		
		}
		
		System.out.println("Maximal Fischer Coefficient: " + maximalFischerCoefficient);
		
		/*
		 * Zwracanie zbioru cech o najwyzszym wspolczynniku Fischera.
		 */
		return combinations.get(maximalFischerIndex);
	}
	
	//FIXME: Problemy z zatrzymywaniem wersji rownoleglej algorytmu.
	
	//TODO: Poprawic wersje rownolegla i uruchomic.
	
	/**
	 * An internal class, representing a concurrent task, which:
	 * <ol>
	 * 	<li>Calculates feature sets on which the children of the currently considered
	 *  node will operate</li>
	 *  <li>Calculates Fischer coefficients for all the children nodes</li>
	 *  <li>In case of the child node's coefficient being greater than the one of the
	 *  current node, triggers parallel investigation of that child node.</li>
	 *  <li>If not, skips the child node, its child nodes and so forth.</li>
	 *  <li>For investigated child nodes, saves their coefficients and feature sets
	 *  in the <code>Map</code> provided.</li>
	 * </ol>
	 * @author Zielony
	 * @version 1.0
	 */
	protected class FeatureSetInvestigator implements Runnable {
		/**
		 * The fischer coefficient of the currently investigated node.
		 */
		double fischerCoefficient;
		
		/**
		 * The dimensions selected at the currently investigated node.
		 */
		int[] selectedDimensions;
		
		/**
		 * The description of the given classes.
		 */
		int[][] classes;
		
		/**
		 * The data the program operates on.
		 */
		Matrix testData;
		
		/**
		 * 
		 * @param selectedDimensions
		 * @param classes
		 * @param testData
		 * @param fischerCoefficient
		 */
		public FeatureSetInvestigator(int[] selectedDimensions, int[][] classes,
				Matrix testData, double fischerCoefficient) {
			this.selectedDimensions = selectedDimensions;
			this.classes = classes;
			this.testData = testData;
			this.fischerCoefficient = fischerCoefficient;
		}
		
		/**
		 * {@inheritDoc}
		 */
		public void run() {
			/*
			 * Jesli skonczyly sie kombinacje wymiarow do badania, zamykanie drzewa
			 * procesów.
			 */
			if(selectedDimensions.length == 0) {
				return;
			}
			
			/*
			 * Wpólczynnik Fischera i-tego dziecka w drzewie.
			 */
			double childCoefficient; 
			
			/*
			 * Wymiary wybierane dla i-tego dziecka w drzewie.
			 */
			int[] childDimensions = new int[selectedDimensions.length-1];
			
			/*
			 * Iteracja po wszystkich dzieciach w drzewie.
			 */
			for(int i=0; i<selectedDimensions.length; i++) {
				/*
				 * Kopiowanie badanych wymiarów z pominieciem i-tego.
				 */
				for(int j=0; j<selectedDimensions.length; j++) {
					if(j<i) {
						childDimensions[j] = selectedDimensions[j];
					}
					else if(j>i) {
						childDimensions[j-1] = selectedDimensions[j];
					}
				}

				/*
				 * Obliczanie wspóczynnika fischera dla i-tego wezla potomnego.
				 */
				childCoefficient = fischerCalculator.calculateFischerCoefficient(
						testData, classes, childDimensions);
				
				/*
				 * Jesli i-ty wezel potomny ma wyzszy wspolczynnik Fischera niz obecny
				 * jest on badany (rownolegly watek), a jego wynik zapisywany do mapy
				 * wyników. W przerciwnym przypadku nie jest ani zapisywany, ani 
				 * badany, wraz ze swoimi wezlami potomnymi.
				 */
				if(childCoefficient >= this.fischerCoefficient) {
					pool.submit(new FeatureSetInvestigator(childDimensions, 
							classes, testData, childCoefficient));
					 
					fischerCoefficientsMap.put(childCoefficient, childDimensions);
				}
			}
		}
	}
}